/*
 * Copyright (C) 2006 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;

import javax.servlet.ServletException;

import com.google.enterprise.sdk.onebox.FailureCode;
import com.google.enterprise.sdk.onebox.Field;
import com.google.enterprise.sdk.onebox.IOneBoxResults;
import com.google.enterprise.sdk.onebox.ModuleResult;
import com.google.enterprise.sdk.onebox.OneBoxProvider;
import com.google.enterprise.sdk.onebox.OneBoxResults;

/**
 * Implementation of a OneBox provider that mimics Keymatch (more or less)
 * 
 */
public class Keymatch extends OneBoxProvider {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private static HashMap<String, KeymatchObj> exactMatchHash = new HashMap<String, KeymatchObj>();
	private static HashMap<String, KeymatchObj> phraseMatchHash = new HashMap<String, KeymatchObj>();
	private static HashMap<String, ArrayList<KeymatchObj>> keywordMatchHash = new HashMap<String, ArrayList<KeymatchObj>>();

	// TODO - handle matchGroups, authType
	String getURL(final String apiMaj, final String apiMin, final String oneboxName, final String dateTime, final String ipAddr,
			final String lang, final String query, final String[] matchGroups) {
		// We don't have access to HttpServletRequest
		final String urlPrefix = getInitParameter("url_prefix");
		return urlPrefix + "apiMaj=" + apiMaj + "&apiMin=" + apiMin + "&oneboxName=" + oneboxName + "&dateTime=" + dateTime
				+ "&ipAddr=" + ipAddr + "&lang=" + lang + "&query=" + query + "&authType=none";
	}

	/*
	 * Replace special characters with XML escapes: & (ampersand) is replaced by &amp; < (less than) is replaced by &lt; >
	 * (greater than) is replaced by &gt; " (double quote) is replaced by &quot;
	 */
	String escapeXML(final String str) {
		return replace(replace(replace(replace(str, "&", "&amp;"), "<", "&lt;"), ">", "&gt;"), "\"", "&quot;");
	}

	String replace(final String str, final String pattern, final String replace) {
		int s = 0;
		int e = 0;
		final StringBuilder result = new StringBuilder(80);
		while ((e = str.indexOf(pattern, s)) >= 0) {
			result.append(str.substring(s, e));
			result.append(replace);
			s = e + pattern.length();
		}
		result.append(str.substring(s));
		return result.toString();
	}

	/**
	 * Canonicalizes the search query: lower case trim leading and trailing spaces convert punctuation to space chars
	 * collapse multiple space chars to a single space TODO - remove special query terms, negative queries, how handle
	 * hyphens and dots
	 */
	private String canonicalizeQuery(final String query) {
		final String lowerQ = query.toLowerCase();
		final String trimLowerQ = lowerQ.trim();
		char[] qArray = new char[trimLowerQ.length()];
		final StringBuilder canonicalQ = new  StringBuilder("");
		qArray = trimLowerQ.toCharArray();
		boolean prevCharIsSpace = false;
		for (int i = 0; i < trimLowerQ.length(); i++) {
			final char ch = qArray[i];
			if (Character.isLetterOrDigit(ch)) {
				canonicalQ.append(ch);
				prevCharIsSpace = false;
			} else if (prevCharIsSpace == false) {
				canonicalQ.append(" ");
				prevCharIsSpace = true;
			}
		}
		// Need to retrim since may introduce a space if the initial char in the
		// query is non-alphanumeric
		return canonicalQ.toString().trim();
	}

	@Override
	public void init() throws ServletException {
		Date d = new Date();
		log("Start loading keymatches: " + d.toString());
		BufferedReader inputStream = null;
		try {
			final String keymatchFile = getInitParameter("keymatch_file");
			inputStream = new BufferedReader(new FileReader(keymatchFile));
			String l;
			while ((l = inputStream.readLine()) != null) {
				// TODO how are commas handled in keymatch files
				final String[] tokens = l.split(",");
				if (tokens.length < 4) {
					continue;
				} else {
					final String keymatch = canonicalizeQuery(tokens[0]);
					final String matchType = tokens[1];
					final String url = tokens[2];
					final String matchString = tokens[3];
					final KeymatchObj keymatchObj = new KeymatchObj(url, matchString, keymatch);
					if (matchType.equals("ExactMatch")) {
						exactMatchHash.put(keymatch, keymatchObj);
					}
					if (matchType.equals("PhraseMatch")) {
						phraseMatchHash.put(keymatch, keymatchObj);
					}
					if (matchType.equals("KeywordMatch")) {
						final String words[] = keymatch.split(" ");
						for (final String element : words) {
							ArrayList<KeymatchObj> matches;
							if (keywordMatchHash.containsKey(element)) {
								matches = keywordMatchHash.get(element);
							} else {
								matches = new ArrayList<KeymatchObj>();
							}
							matches.add(keymatchObj);
							keywordMatchHash.put(element, matches);
						}
					}
				}
			}
		} catch (final IOException ioe) {
			final StringWriter sw = new StringWriter();
			final PrintWriter pw = new PrintWriter(sw);
			ioe.printStackTrace(pw);
			log(sw.toString());
		} finally {
			if (inputStream != null) {
				try {
					inputStream.close();
				} catch (final IOException ioe2) {
				}
			}
		}
		d = new Date();
		log("Finish loading keymatches: " + d.toString());
	}

	/**
	 * 
	 * 
	 * @param apiMaj
	 * @param apiMin
	 * @param oneboxName
	 * @param dateTime
	 * @param ipAddr
	 * @param lang
	 * @param query
	 * @param matchGroups
	 * @return
	 */
	@Override
	protected IOneBoxResults provideOneBoxResults(final String apiMaj, final String apiMin, final String oneboxName,
			final String dateTime, final String ipAddr, final String lang, final String query, final String[] matchGroups) {

		final String thisURL = getURL(apiMaj, apiMin, oneboxName, dateTime, ipAddr, lang, query, matchGroups);

		// Create results object
		final OneBoxResults res = new OneBoxResults();
		res.setProviderText("keymatch");

		// Check api version for compatibility
		final int apiVersion = ((Integer.parseInt(apiMaj) << 0xFF) | (Integer.parseInt(apiMin)));
		if (apiVersion < ((1 << 0xFF) | 0)) { // 1.0
			res.setFailure(FailureCode.lookupFailure, "OneBox API versions older than 1.0 not supported by provider");
			return res;
		}

		int matchCount = 0;
		final String canonicalQuery = canonicalizeQuery(query);
		log("Canonical query: " + canonicalQuery);

		if (exactMatchHash.containsKey(canonicalQuery)) {
			if (res.canAddResult()) {
				final KeymatchObj ko = exactMatchHash.get(canonicalQuery);
				final ModuleResult mr = new ModuleResult(escapeXML(ko.matchString), escapeXML(ko.url));
				mr.addField(new Field("matchType", "ExactMatch"));
				mr.addField(new Field("hashLookup", canonicalQuery));
				mr.addField(new Field("keymatch", ko.keymatch));
				res.addResult(mr);
				matchCount++;
			}
		}

		// First get the list of possible keymatches
		final String[] words = canonicalQuery.split(" ");
		final ArrayList<KeymatchObj> allMatchObjects = new ArrayList<KeymatchObj>();
		for (final String element : words) {
			if (keywordMatchHash.containsKey(element)) {
				final ArrayList<KeymatchObj> matchObjects = keywordMatchHash.get(element);
				for (int j = 0; j < matchObjects.size(); j++) {
					final KeymatchObj ko = matchObjects.get(j);
					if (!allMatchObjects.contains(ko)) {
						allMatchObjects.add(ko);
					}
				}
			}
		}
		// Now determine which keymatches are complete
		int i = 0;
		while (i < allMatchObjects.size()) {
			final KeymatchObj ko = allMatchObjects.get(i);
			// Each of these keymatch words must be in the canonicalized query words
			final String keymatchWords[] = ko.keymatch.split(" ");
			final ArrayList<String> canonicalWords = new ArrayList<String>();
			for (final String element : words) {
				canonicalWords.add(element);
			}
			for (int k = 0; k < keymatchWords.length; k++) {
				if (!canonicalWords.contains(keymatchWords[k])) {
					allMatchObjects.remove(i);
					i--;
					break;
				}
			}
			i++;
		}

		for (int j = 0; j < allMatchObjects.size(); j++) {
			if (res.canAddResult()) {
				final KeymatchObj ko = allMatchObjects.get(j);
				log("Keymatch match: " + ko.toString());
				final ModuleResult mr = new ModuleResult(escapeXML(ko.matchString), escapeXML(ko.url));
				mr.addField(new Field("matchType", "Keyword"));
				mr.addField(new Field("hashLookup", canonicalQuery));
				mr.addField(new Field("keymatch", ko.keymatch));
				res.addResult(mr);
				matchCount++;
			}
		}

		final Enumeration phrases = new Phrases(canonicalQuery);
		String phrase;
		for (; phrases.hasMoreElements();) {
			phrase = (String) phrases.nextElement();
			log("Phrase: " + phrase);
			if (phraseMatchHash.containsKey(phrase)) {
				if (res.canAddResult()) {
					final KeymatchObj ko = phraseMatchHash.get(phrase);
					final ModuleResult mr = new ModuleResult(escapeXML(ko.matchString), escapeXML(ko.url));
					mr.addField(new Field("matchType", "PhraseMatch"));
					mr.addField(new Field("hashLookup", phrase));
					mr.addField(new Field("keymatch", ko.keymatch));
					res.addResult(mr);
					matchCount++;
				}
			}
		}

		if (matchCount == 0) {
			res.setFailure(FailureCode.lookupFailure, "No matches found for: " + canonicalQuery);
		} else {
			res.setResultsTitleLink("Matches: " + matchCount, escapeXML(thisURL));
		}
		return res;
	}
}

class KeymatchObj {

	String url;
	String matchString;
	String keymatch;

	KeymatchObj(final String url, final String matchString, final String keymatch) {
		this.url = url;
		this.matchString = matchString;
		this.keymatch = keymatch;
	}

	@Override
	public String toString() {
		return "URL: " + url + " MatchString: " + matchString + " Keymatch: " + keymatch;
	}
}

class Phrases implements Enumeration {

	String[] words;
	int i = 0;
	int j = 0;
	boolean hasMore = true;

	Phrases(final String canonicalQuery) {
		this.words = canonicalQuery.split(" ");
	}

	public boolean hasMoreElements() {
		return hasMore;
	}

	public Object nextElement() {
		if (!hasMore) {
			return null;
		}
		final StringBuilder next = new StringBuilder(80);
		for (int k = 0; k <= j; k++) {
			if (k != 0) {
				next.append(" ");
			}
			next.append(words[i + k]);
		}
		// System.out.println(next.toString());
		moveIndex();
		return next.toString();
	}

	void moveIndex() {
		// System.out.println("Calling moveIndex with i = " + i + " and j = " + j);
		if (i <= words.length - 1 && i + j <= words.length - 2) {
			j++;
		} else if (i <= words.length - 2) {
			i++;
			j = 0;
		} else {
			hasMore = false;
		}
		// System.out.println("Return from moveIndex with i = " + i + " and j = " + j);
	}

	public static void main(final String[] args) {
		final Phrases phrases = new Phrases("foo bar baz");
		for (; phrases.hasMoreElements();) {
			final String phrase = (String) phrases.nextElement();
		}
	}

}
